Abzugeben ist das Jupyter Notebook mit dem verlangten Implementierungen, den entsprechenden Ausgaben, Antworten und Diskussionen/Beschreibungen. Das Notebook ist als .ipynb und als .html abzugeben.
In diesem Versuch sollen Kenntnisse in folgenden Themen vermittelt werden:
Machen Sie sich mit den Grundlagen von Pandas vertraut.
Machen Sie sich mit den Grundlagen von Bokeh vertraut.
Machen Sie sich mit der Hauptachsentransformation (PCA) und dem t-SNE Verfahren vertraut.
Machen Sie sich mit Linearer Regression und Random Forest Regression vertraut.
Machen Sie sich mit dem k-means Clustering Algorithmus vertraut.
In diesem ersten Teil des Versuchs müssen alle relevanten Daten aus .csv-Files eingelesen und in PostgreSQL-Tabellen abgelegt werden. Alle benötigten .csv Files befinden sich im Verzeichnis gesundheitsdaten. Die Daten stammen aus folgenden Quellen:
Das unten gegebene Dictionary filenames_general definiert aus welchen Dateien (keys), welche Spalten (values) gebraucht werden. Die Bezeichnung der Spalten in den Files weicht von der Spaltenbezeichnung im Dictionary ab. Die im Dictionary angegebenen Namen sollen aber in diesem Versuch (in den Pandas Dataframes und in den Datenbank-Tabellen) verwendet werden.
filenames_general = {'life-expectancy.csv':['Entity','Code','Year','LifeExpectancy'],
'gdp-per-capita-worldbank.csv':['Entity','Code','Year','GDPperCapita'],
'annual-healthcare-expenditure-per-capita.csv':['Entity','Code','Year','AnnualHealthcarExpPerCapita'],
'annual-working-hours-per-persons-engaged.csv':['Entity','Code','Year','AnnualWorkingHourPerPerson']
}
Tipp: Verwenden Sie für das Zusammenführen der Daten aus verschiedenen Files die Methode
merge()des Pandas-Dataframes.
an.
to_sql() in eine Datenbanktabelle mit dem Namen general_information.import psycopg2 #provides drivers for PostgreSQL
import numpy as np
np.set_printoptions(precision=2,suppress=True)
import json #required to access json file
import pandas as pd
import geopandas as gpd
import pprint
pp = pprint.PrettyPrinter(indent=4)
# The code reads the contents of the .json file into a Python dictionary.
with open('../db/configLocalP1.json') as f:
conf = json.load(f)
# 1. Aufgabe
# Parse data from the provided csv files via Pandas library
result = pd.DataFrame
result2 = pd.DataFrame
general_information = pd.DataFrame
for key in filenames_general:
csv_file = '../data/gesundheitsdaten_{}'.format(key)
general_information = pd.read_csv(csv_file, names=filenames_general[key], header=0)
general_information = general_information.dropna(subset=['Code', 'Year']) # Drop missing values.
if result.empty:
result = general_information
else:
result = result.merge(general_information, how='left', on=['Entity' ,'Code', 'Year'])
#print('Merged result1:{}'.format(result.head(10)))
# 2. Aufgabe
# Print 10 values to check
print(general_information.head(10))
# Number of rows
print('Total number of rows: {}'.format(general_information.shape[0]))
# Number of columns
print('Total number of columns: {}'.format(general_information.shape[1]))
# Investigating missing values
print(general_information.isnull().sum(axis = 0))
# Create connection string to connect to PostgreSQL database
from sqlalchemy import create_engine
conn_str ='postgresql://{}:{}@localhost:{}/{}'.format(conf['user'], conf['passw'], conf['port'], conf['database'])
engine = create_engine(conn_str)
# 3. Aufgabe
table_name = 'general_information'
# Check if datatable already exists,then try to
# Write records stored in a dataFrame to SQL database.
if not engine.has_table(table_name):
try:
result.to_sql(name=table_name, index=False, con=engine)
except (MySQLdb.Error, MySQLdb.Warning) as e:
print('Error {} occured writing to database.'.format(e))
else:
print('The table {} already exists.'.format(table_name))
general_information = result.copy()
Lesen Sie nun gleich wie oben die Ernährungsdaten ein. Die relevanten Spalten der entsprechenden Files sind nun im Dictionary filenames_nutrition definiert. Diese Daten sind in einer Datenbanktabelle nutrition_information anzulegen.
filenames_nutrition = {
'life-expectancy.csv':['Entity','Code','Year','LifeExpectancy'],
'fruit-consumption-per-capita-kilograms-per-year.csv':
['Entity','Code','Year','AnnualFruitConsumptionPerCapita'],
'vegetable-consumption-per-capita-kilograms-per-year.csv':
['Entity','Code','Year','AnnualVegetableConsumptionPerCapita'],
'dietary-compositions-by-commodity-group-1961-2013.csv':
['Entity','Code','Year','KcalOther','KcalSugar',
'KcalOilsFats','KcalMeat','KcalDairyEggs',
'KcalFruitsVegetables','KcalStarchyRoots','KcalPulses',
'KcalCerealsGrains','KcalAlcoholicBeverages'],
'daily-caloric-supply-derived-from-carbohydrates-protein-and-fat.csv':
['Entity','Code','Year','KcalAnimalProtein',
'KcalPlantProtein','KcalFat','KcalCarbohydrates'],
'daily-per-capita-supply-of-calories.csv':['Entity','Code','Year','DailyCaloriesPerCapita']
}
# Parse data from the provided csv files via Pandas library
result = pd.DataFrame
nutrition_information = pd.DataFrame
for key in filenames_nutrition:
csv_file = '../data/gesundheitsdaten_{}'.format(key)
nutrition_information = pd.read_csv(csv_file, names=filenames_nutrition[key], header=0)
nutrition_information = nutrition_information.dropna(subset=['Code', 'Year']) # Drop missing values.
# Plot the first rows, columns of the Pandas data frame
print('First lines in dataframe:\n{}'.format(nutrition_information.head()))
# Investigating over missing values
print('Information about dataframe:\n{}'.format(nutrition_information.info()))
if result.empty:
result = nutrition_information
else:
result = result.merge(nutrition_information, how='left', on=['Entity' ,'Code', 'Year'])
table_name = 'nutrition_information'
# Check if datatable already exists,then try to
# Write records stored in a DataFrame to SQL database.
if not engine.has_table(table_name):
try:
result.to_sql(name=table_name, index=False, con=engine)
except (MySQLdb.Error, MySQLdb.Warning) as e:
print('Error {} occured writing to database.'.format(e))
else:
print('The table {} already exists.'.format(table_name))
nutrition_information = result.copy()
general_information in einen Pandas Dataframe.Yearund AnnualWorkingHourPerPerson für alle zu Germany gehörende Zeilen aus der Tabelle general_information in einen Pandas Dataframe.Anmerkung: Für diesen Versuch kommen relativ kleine Datenmengen zum Einsatz. In diesem Fall kann direkt auf den Pandas Dataframes gearbeitet werden. Für sehr große Datenmengen bietet es sich an, nicht alle Daten in den Arbeitsspeicher zu laden, sondern nur die aktuell benötigten durch entsprechende Datenbankabfragen.
# 1. Aufgabe
giTable = pd.read_sql("general_information", engine)
display(giTable.head(5))
# 2. Aufgabe
sqlQuery = """SELECT "Year", "AnnualWorkingHourPerPerson" FROM general_information WHERE "Entity" = 'Germany'"""
giTable2 = pd.read_sql(sqlQuery ,engine)
display(giTable2.head())
Über die Zeit kann man erkennen, dass die Arbeitsstunden pro Jahr in Deutschland abnehmen. Dies könnte im Zusammenhang mit der zunehmenden Automatisierung/Industrialisierung von Arbeitstätigkeiten stehen.
In diesem Teilversuch soll die Lebenserwartung pro Land in einer Weltkarte visualisiert werden. Zu erstellen ist ein Plot der unten dargestellten Art:

# Path to the geographical data stored in a .json file
PATH = '../data/json/gesundheitsdaten_'
# Load data into GeoDataFrame
countries = gpd.read_file(PATH + 'countries_geo.json')
# Examine country GeoDataFrame loaded from .json file
countries.head()
import descartes
# Test plot the countries with GeoPandas Descartes library
countries.plot()
countries mit dem Dataframe general_information. Achten Sie hierbei, dass Sie den pandas dataframe in den geopandas dataframe mergen, das Resultat also ein geopandas dataframe mergen ist (andernfalls funktioniert der nächste Schritt nicht). # 1. Aufgabe
general_information_2015 = giTable.loc[giTable["Year"] == 2015][["Code", "LifeExpectancy"]]
# 2. Aufgabe
# Adjust names for the values of "Entity" and "Code" to "name" and "id"
general_information_2015.rename(columns={"Code":"id"}, inplace=True)
# Check the new values
print ('The new name-values are \n{}'.format(general_information_2015))
# Merge the two dataframes into a GeoDataFrame
countriesMerged = countries.merge(general_information_2015, how='left', on=["id"])
#print('The merged GeoDataFrame values are \n{}'.format(countriesMerged))
to_json() kann ein Dataframe in json transformiert werden. Die von der Methode zurückgegebenen Daten können einem Bokeh GeoJSONDataSource-Objekt beim Anlegen übergeben werden. Das GeoJSONDataSource-Objekt kann direkt der entsprechenden Bokeh-plotting Funktion übergeben werden. Erzeugen Sie mit diesen Hinweisen einen interaktiven Weltkartenplot der oben dargestellten Art. Beim Mouse-Over über ein Land soll der Name des Landes und die Lebenserwartung angezeigt werden.Alternative: Eine einfachere Lösung für die Geovisualisierung bietet geopandas. Allerdings sind damit keine interaktiven Elemente realisierbar.
# Write the GeoPandaFrame to GeoJSON file format
countriesMerged.to_file("../data/json/lifeexp_geodata_2015.json", driver="GeoJSON")
# Import all required modules to render the interactive map in Bokeh
from bokeh.io import output_notebook, show
from bokeh.models import GeoJSONDataSource, LinearColorMapper, ColorBar
from bokeh.plotting import figure
from bokeh.palettes import Viridis256
import json
The merged files result in a GeoDataframe object, which can be represented by using a geopandas module. For in interactive visualization Bokeh library is used. Bokeh consumes GeoJSON format, which represents geographical features with JSON. GeoJSON describes points, lines and polygones (Patches) as a collection of features. Therefore the merged files are converted to a GeoJSON format.
# 3. Aufgabe
#GeoJSONDataSource Objekt erstellen
data = json.loads(countriesMerged.to_json())
# Convert the GeoJson data to a string-like object
geo_source = GeoJSONDataSource(geojson=json.dumps(data))
#Bokeh ausgabe im Notebook
output_notebook()
#Tooltip für Mouse-Over
TOOLTIPS = [
('Land', '@name'),
('Lebenserwartung', '@LifeExpectancy')
]
#Color Mapper für farbliche Visualisierung erstellen
Viridis256 = tuple(reversed(Viridis256))
color_mapper = LinearColorMapper(palette=Viridis256, low=countriesMerged.LifeExpectancy.min(), high=countriesMerged.LifeExpectancy.max())
color_mapper.low_color = "grey"
#Color bar als Legende
color_bar = ColorBar(color_mapper=color_mapper,
label_standoff=12, border_line_color=None, location=(0,0))
#bokeh figure anlegen, mit zuvor erstelltem tooltip
p = figure (background_fill_color="lightgrey", plot_height = 600, plot_width= 1200, tooltips=TOOLTIPS)
#Länder hinzufügen mit farblich codierter Lebenserwartung
p.patches('xs', 'ys', source=geo_source, line_color="black", line_width=0.25, fill_alpha=1, fill_color = {'field' :'LifeExpectancy', 'transform' : color_mapper})
#Color bar der bokeh figure hinzufügen
p.add_layout(color_bar, 'right')
show(p)
Auf der mittels Bokeh visualisierten Weltkarte sind die Lebenserwartungen je Land dargestellt. Hierbei werden die einzelnen Länder mit einer Farbskala von dunkel-lila bis hell-gelb eingefärbt. Je dunkler die Farbgebung, desto höher die Lebenserwartung.
Bei der Analyse der Weltkarte ist besonders auffällig, dass sowohl für Serbien, Somalia, Grönland, als auch Antarktika (sowie kleinere Inseln) keine Daten in der CSV Datei vorhanden sind. Diese werden hier grau dargestellt. Dagegen ist die Lebenserwartung in industriell weiter entwickelten Nationen, wie Japan (~ ca. 83 Jahre), Australien (~ ca. 82.7 Jahre), Kanada (~ ca. 82.5 Jahre), Neuseeland (~ ca. 81 Jahre), als auch Zentral-Europa am höchsten. Die niedrigste Lebenserwartungen sind vor allem in Zentral-Afrika (~ ca. 50 - 60 Jahre), Indien (~ ca. 68 Jahre), sowie Russland (~ ca. 70.9 Jahre), als auch dem Süden Asiens zu verzeichnen.
Der Weltkarte nicht entnehmbar sind dabei politische, als auch gesellschaftliche Entwicklungen. Dennoch kann man aus dem Allgemeinwissen heraus schließen, dass sich die Geschichte u.A. Kolonialisierung der Länder in der Vergangenheit stark auf die Lebenserwartung und den Wohlstand in der Bevölkerung ausgewirkt hat.
In diesem Abschnitt soll die paarweise Korrelation von
GDPperCapitaAnnualHealthcarExpPerCapitamit der LifeExpectancy untersucht werden.
LifeExpectancy über GDPperCapita bzw. über AnnualHealthcarExpPerCapita dargestellt ist.numpy-Funktion corrcoef die beiden paarweisen Korrelationen. import seaborn as sns
import matplotlib.pyplot as plt
# 1. Aufgabe
fig, ax = plt.subplots(1,2, figsize=(20,10))
sns.scatterplot(y="LifeExpectancy", x="GDPperCapita", data=giTable.dropna(), ax=ax[0])
sns.scatterplot(y="LifeExpectancy", x="AnnualHealthcarExpPerCapita", data=giTable.dropna(), ax=ax[1])
plt.show()
# 2. Aufgabe
corr = np.corrcoef(giTable[["LifeExpectancy","GDPperCapita","AnnualHealthcarExpPerCapita"]].dropna(), rowvar=False)
print("Korrelationen: \n"
"GDPperCapital zu LifeExpectancy: " + str(corr[1][0]) + "\n"
"AnnualHealthcarExpPerCapital zu LifeExpectancy: " + str(corr[2][0]) + "\n")
Für beide Werte gilt: Für niedrige Werte haben die Merkmale geringen Einfluss auf die Lebenserwartung. Ab circa 10.000 GDPperCapita und ab circa 1.000 AnnualHealthcarExpPerCapita lässt sich eine starke Korrelation erkennen. Insgesamt haben wir eine moderate Korrelation da die Merkmale anfänglich kaum korrelieren.
Untersuchen Sie dann so wie im vorigen Abschnitt wie die einzelnen Nuitrition-Merkmale mit der LifeExpectancy korrelieren. Diskutieren Sie das Ergebnis.
nutritionTable = nutrition_information
#Bereich für mehrere Plots erstellen
fig, ax = plt.subplots(nrows=6, ncols=3, figsize=(25,30))
#plots hinzufügen
for i, col in enumerate(nutritionTable.columns[3:]):
if col != "LifeExpectancy":
sns.scatterplot(y="LifeExpectancy", x=col, data=nutritionTable.dropna(), ax=ax[(i-1)//3,(i-1)%3])
#leeren Plot entfernen
fig.delaxes(ax[5][2])
#Plot anzeigen
plt.show()
#Korrelationten zwischen den Merkmalen und Lebenserwartung
display(nutritionTable.corr()[["LifeExpectancy"]])
Auf der Basis des Dataframes nutrition_information: Stellen Sie die zeitliche Entwicklung des Merkmals
KcalFat DailyCaloriesPerCapita von 1995 bis 2015 für die Länder Bolivia, Bulgaria, Egypt, Kenya, United States, China, Brazil, Germany graphisch dar. Erzeugen Sie hierfür pro Merkmal einen Plot, in dem die Linegraphs aller genannten Länder dargestellt sind.
Interpretieren Sie diese Darstellungen
%matplotlib inline
from matplotlib import pyplot as plt
from matplotlib.pylab import rcParams
plt.style.use('ggplot')
# Additional Style elements
plt.rcParams['lines.linewidth'] = 3.5
plt.rcParams['lines.linestyle'] = '--'
plt.rcParams['lines.marker'] = 'o'
plt.rcParams['axes.titlepad'] = 25
plt.rcParams['axes.labelpad'] = 20
# Filter the dataframe
dfToPlot = nutrition_information.loc[(nutrition_information['Entity'].isin(['Bolivia', 'Bulgaria', 'Brazil', 'China', 'Egypt', 'Germany', 'Kenya', 'United States'])) & (nutrition_information['Year'] >= 1995)]
dfToPlot.head(5)
# List of countries for time series analysis
countries = dfToPlot['Entity'].unique()
# Data preprocessing to fill in NaN values with interpolation
result = pd.DataFrame()
# Iterate through each selection
for country in countries:
result = nutrition_information.loc[nutrition_information['Entity'] == country]
nutrition_information.loc[nutrition_information['Entity'] == country] = result.fillna(result.interpolate(method='linear', limit_area='inside'))
# Analyse the tail of the dataframe
nutrition_information.tail(5)
Fehlende Datenwerte zwischen den Jahren 2013 und 2015 können mit verschiedenen Techniken ergänzt werden. Eine Möglichkeit ist diese mit der Funktion interpolate() des Pandas Series Objektes linear zu interpolieren. Als Startpunkt wurde hier eine einfache, lineare Interpolation gewählt.
# Convert column 'Year' to datetime for time-indexing
dfToPlot.loc['Year'] = pd.to_datetime(dfToPlot['Year'], format='%Y', infer_datetime_format=True).dt.year
# Indexing with time-series data to create datetime index
dfToPlot = dfToPlot.set_index(['Year'])
# Summary of the dataset
dfToPlot.isnull().sum()
dfToPlot.tail()
# 1. Aufgabe DailyCaloriesPerCapita Plot
ax = plt.gca()
for country in countries:
y = dfToPlot.loc[dfToPlot['Entity'] == country]
y = y['KcalFat']
y.plot(kind='line', xticks=y.index, figsize=(45, 20), grid=True, ax=ax)
plt.legend(countries, fontsize=35, bbox_to_anchor=(1.05, 1), loc='upper left', borderaxespad=0.1)
plt.xlabel('Year', fontsize=35)
plt.ylabel('Fat Consumption in Kcal', fontsize=35)
plt.title('Time Series Analysis, Fat Consumption per Year', fontsize=50)
plt.show()
# Store png
plt.savefig('../data/png/time_series_fatcons.png')
# 2.Aufgabe
ax = plt.gca()
for country in countries:
y = dfToPlot.loc[dfToPlot['Entity'] == country]
y = y['DailyCaloriesPerCapita']
y.plot(xticks=y.index, figsize=(40,20), grid=True, ax=ax)
plt.legend(countries, fontsize=25, bbox_to_anchor=(1.05, 1), loc='upper left', borderaxespad=0.1)
plt.xlabel('Year', fontsize=25)
plt.ylabel('Fat Consumption in Kcal', fontsize=25)
plt.title('Time Series Analysis, Daily Calories per Capita and Year', fontsize=30)
plt.show()
# Store png
plt.savefig('../data/png/time_series_caloriespercap.png')
Die zu untersuchende, erste Grafik 'Time Series Analysis, Annual Fat Consumption (Kcal) ', als auch die zweite Grafik 'Time Series Analysis, daily Calories per Capita' zeigen die Entwicklung des Kalorien- per Capita, als auch Fett Konsums, innerhalb verschiedener Länder, in Abhängigkeit der Zeit. Zur Untersuchung verschiedener Gesetzmäßigkeiten werden die Werte von acht, global verteilten Ländern herangezogen. Diese umfassen fünf Schwellenländer, die derzeit noch zu den globalen Entwicklungsländern zählen, sowie drei Industrie Nationen. Die Entwicklungsländer liegen im südamerikanischen Raum Bolivien, als auch Brasilien, sowie im Wirtschaftsraum EMEA mit Bulgarien und Ägypten. Dazu kommt Zentralafrika mit Kenya. Dem gegenüber stehen drei Industrienationen, sowohl im asiatischen Raum mit China, als auch zwei westlich Nationen aus Zentral Europa, sowie Nord Amerika mit Deutschland, sowie den Vereinigten Amerikanischen Staaten. Die dargestellten Werte haben einen jährlich wiederkehrenden Messzeitpunkt und verteilen sich über einen Zeitabschnitt von 20 Jahren zwischen den Jahren 1995 und 2015.
Bei der Analyse beider Grafiken ist insgesamt ein kontinuierlicher Aufwärtstrend erkennbar. Die Zeitreihen weisen langfristige Veränderungen in ihrem Niveau auf und sind damit nichtstationär. Gleichzeitig unterliegen die Zeitreihen innerhalb des Zeitraumes keinen saisonellen Entwicklungen. Sich wiederholende Entwicklungen innerhalb eines Jahres können in den vorliegenden Zeitreihendaten jedoch nicht ausgemacht werden, da für die Analyse (z.B. bei saisonellen Schwankungen zwischen Sommer und Wintermonaten) die einzelnen Zeitangaben fehlen. Auch starke Ausreißer innerhalb der Zeitreihe, sowie zyklische Enwicklungen sind nicht erkennbar.
In der ersten Grafik ist ein deutlicher Niveau Unterschied zwischen den verschiedenen Ländern erkennbar. So befinden sich die Industrienationen USA mit einem Mittelwert von 1403 Kcal/Jahr, sowie Deutschland mit einem Mittelwert von 1276 Kcal/Jahr am oberen Ende der Skala, während Bolivien mit einem Mittelwert von 426 Kcal/Jahr, neben Kenya mit 431 Kcal/Jahr, den geringsten Fett Konsum verzeichnet. Dies korreliert mit den Ergebnissen des pro Kopf Kalorienverbrauchs der zweiten Grafik. Die USA hat auch hier, dicht gefolgt von Deutschland mit 3415 Kalorien/Tag, den größten pro Kopf Verbrauch mit täglich 3701 Kalorien. Zum Vergleich benötigt ein Mann zwischen 25 und 51 Jahre durchschnittlich 2.400 Kalorien am Tag. Dies lässt darauf schließen, dass sowohl die deutsche, als auch die nordamerikanische Bevölkerung vermehrt an Adipositas, sowie Folgekrankheiten leidet.
Auffällig ist bei Betrachtung von Unregelmäßigkeiten, dass seit dem Jahr 2005 eine negative Trendwende in den Industriestaaten, vor allem den USA statt gefunden hat. Während der Fettverbrauch pro Jahr nur leicht zurück ging, wurde der Kalorienverbrauch pro Kopf deutlich rückläufig. Bei nahezu allen Entwicklungsländern ging dagegen der Verbrauch insgesamt mit einem positiven Trend nach oben und man kann hier davon ausgehen, in Abhängigkeit der wirtschaftlichen Lage des jeweiligen Landes, dass sich dieser Aufwärtstrend auch in Zukunft weiter positiv entwickeln wird.
nutrition_information existieren.general_information ohne die Spalte AnnualWorkingHourPerPerson. Der so gebildete Dataframe wird im Weiteren mit data_nut_gen bezeichnet.# 1. Aufgabe
# Neues DataFrame das alle Jahre und die Anzahl der non-Nans in dieser Spalte enthält
df = pd.DataFrame()
df['Year'] = nutrition_information['Year']
df['non-Nan'] = nutrition_information.count(axis=1)
# Dictionary in dem zu jedem Jahr die Anzahl an Non-Nan Spalten zugewiesen wird
Non_nans_count = {}
non_nans_count = []
# Maximale Anzahl an Spalten die nicht Null sind
maximal = df["non-Nan"].max()
for label, row in df.iterrows():
# Wenn die Anzahl an Non-Nan Spalten dem Maximum entspricht, wird es der Liste 'non_nans_count' angefügt
if row[1] == maximal:
non_nans_count.append(row[0])
# Für jedes Jahr wird gezählt wie viele Non-Nan Spalten in der Liste 'non_nans_count' enthalten sind
for i in nutrition_information['Year'].unique():
# Für jedes Jahr (=Key) wird die Anzahl (=Value) dem Dictionary 'Non_nans_count' zugewiesen
Non_nans_count[i] = non_nans_count.count(i)
[k for k,v in Non_nans_count.items() if v == max(Non_nans_count.values())]
# 2. Aufgabe
# Entscheidung fiel für das Jahr 2013
# Datenbankabfrage um alle Daten von der table 'nutrition_information' vom Jahr 2013 zu selektieren
query = """SELECT * FROM nutrition_information WHERE "Year"= '2013'"""
df_2013 = pd.read_sql_query(query,engine)
# Drop all NaN values
df_2013 = df_2013.dropna()
# Datenbankabfrage um alle Daten außer 'AnnualWorkingHourPerPerson' von der table 'general_information' vom Jahr 2013 zu selektieren
query = """SELECT "Entity", "Code", "Year","LifeExpectancy","GDPperCapita","AnnualHealthcarExpPerCapita" FROM general_information WHERE "Year"= '2013'"""
general_information_2013 = pd.read_sql_query(query,engine)
# 3. Aufgabe
# Merge vom DataFrame df_2013 und general_information_2013
data_nut_gen = general_information_2013.merge(df_2013, how="left", on=["Entity","Code","Year","LifeExpectancy"])
# Löschen von noch vorhandenen NaN Spalten
data_nut_gen = data_nut_gen.dropna()
# Resetet Index weil diese nach dem drop lückenhaft sind
data_nut_gen.index = range(len(data_nut_gen))
# Show first values
data_nut_gen.head(5)
Der oben konstruierte Dataframe data_nut_gen sollte ausschließlich non-NAN Werte haben und Daten nur eines Jahres enthalten. Alle Spalten von data_nut_gen, außer Entity, Code, Year und LifeExpectancy werden im Folgenden als Merkmalsspalten bezeichnet.
explained_variance_ratio. Was sagen diese Zahlen aus?data_nut_gen in einem Bokeh-Plot dar. In diesem soll die Farbe der Punkte durch die Werte der Spalte LifeExpectancy codiert werden.from sklearn.decomposition import PCA
# 1. Aufgabe
import numpy as np
from sklearn.decomposition import PCA
from sklearn.preprocessing import StandardScaler
features = ['GDPperCapita', 'AnnualHealthcarExpPerCapita', 'AnnualFruitConsumptionPerCapita', 'AnnualVegetableConsumptionPerCapita',
'KcalOther', 'KcalSugar', 'KcalOilsFats']
X = data_nut_gen.loc[:, features].values
scaler = StandardScaler()
scaler.fit(X)
X = scaler.transform(X)
pca = PCA(n_components=2)
pca.fit(X)
X_pca = pca.transform(X)
print("original shape: ", X.shape)
print("transformed shape: ", X_pca.shape)
Df = pd.DataFrame(data = X_pca, columns = ['principal_component_1', 'principal_component_2'])
finalDf = pd.concat([Df,data_nut_gen['LifeExpectancy']], axis = 1)
# 2. Aufgabe
# explained_variance_ratio_ beider Hauptkomponenten
print('explained_variance_ratio_: ', pca.explained_variance_ratio_)
# explained_variance_ratio_ von Hauptkomponente 1
print('Hauptkomponente 1 explained_variance_ratio_: ', pca.explained_variance_ratio_[0])
# explained_variance_ratio_ von Hauptkomponente 2
print('Hauptkomponente 2 explained_variance_ratio_: ', pca.explained_variance_ratio_[1])
# Summe beider Komponenten
print('Summe beider Hauptkomponenten: ', pca.explained_variance_ratio_[0]+pca.explained_variance_ratio_[1])
'explained variance ratio' gibt an, wie viel Information (variance) den einzelnen Hauptkomponenten zugeordnet werden kann. Diese ist wichtig, da man z.B. einen 7-dimensionalen Raum in einen 2-dimensionalen Raum umwandeln kann, dabei aber etwas von der Varianz verliert. In unserem Beispiel hat die erste Hauptkomponente eine Varianz von ~48,28% und die zweite Hauptkomponente eine Varianz von ~15,09%. Daraus ergibt sich, dass beide Komponenten zusammen noch ~63,37% der Information enthalten.
# 3. Aufgabe
#Color_mapper und color_bar zum farblichen codieren der Lebenserwartung
color_mapper = LinearColorMapper(palette=Viridis256, low=finalDf.LifeExpectancy.min(), high=finalDf.LifeExpectancy.max())
color_bar = ColorBar(color_mapper=color_mapper,
label_standoff=12, border_line_color=None, location=(0,0))
TOOLTIPS = [
('Lebenserwartung', '@LifeExpectancy')
]
#Scatterplot erstellen
p = figure(plot_height = 500, plot_width= 500, background_fill_color="lightgrey",tooltips=TOOLTIPS)
p.scatter('principal_component_1', 'principal_component_2', source=finalDf, color={'field': 'LifeExpectancy', 'transform': color_mapper})
p.add_layout(color_bar, 'right')
show(p)
from sklearn.manifold import TSNE
from sklearn.preprocessing import StandardScaler
# 4. Aufgabe
#Daten filter und numpy array in numpy array umwandeln
data_nut_gen_tsne = data_nut_gen.reset_index()[data_nut_gen.columns.difference(['Entity', 'Code', 'Year', 'LifeExpectancy'])]
data_nut_gen_tsne = data_nut_gen_tsne.to_numpy()
#Daten standardisieren
scaler = StandardScaler()
scaler.fit(data_nut_gen_tsne)
data_nut_gen_tsne = scaler.transform(data_nut_gen_tsne)
#TSNE durchführen
data_nut_gen_tsne = TSNE(n_components=2, perplexity=10).fit_transform(data_nut_gen_tsne)
#Ergebnis in DataFrame wandeln
data_nut_gen_tsne = pd.DataFrame(data = data_nut_gen_tsne, columns = ['tsne_1', 'tsne_2'])
#Spalte mit Lebenserwartung anhängen
data_nut_gen_tsne = pd.concat([data_nut_gen_tsne, data_nut_gen.reset_index()[['LifeExpectancy']]], axis = 1)
#Color mapper/bar für farbliche Visualisierung der Lebenserwartung
color_mapper = LinearColorMapper(palette=Viridis256, low=data_nut_gen_tsne.LifeExpectancy.min(), high=data_nut_gen_tsne.LifeExpectancy.max())
color_bar = ColorBar(color_mapper=color_mapper,
label_standoff=12, border_line_color=None, location=(0,0))
#Bokeh Plot erstellen
p = figure(plot_height = 600, plot_width= 600, background_fill_color="lightgrey")
p.scatter('tsne_1', 'tsne_2', source=data_nut_gen_tsne, color={'field': 'LifeExpectancy', 'transform': color_mapper},radius=0.5)
p.add_layout(color_bar, 'right')
show(p)
Bei beiden Dimensionsreduktionsverfahren lässt sich ein Verlauf der Lebenserwartung erkennen. Bei der PCA sind die einzelnen Punkte in einer "Wolke" angeordnet. Beim TSNE sind die Punkte weiter verteilt und ähnliche Lebenserwartungen sind näher beieinander. Die Länder sind klarer gruppiert. Cluster sind besser zu erkennen.
Im Folgenden soll aus den Merkmalsspalten das Dataframes data_nut_gen die Lebenserwartung (Spalte LifeExpectancy) vorhergesagt werden.
from sklearn.linear_model import LinearRegression
from sklearn.model_selection import train_test_split
from sklearn.metrics import r2_score, mean_squared_error, mean_absolute_error
# Investigation of data quality
data_nut_gen[data_nut_gen.isnull().any(axis=1)][:5]
# 1. Aufgabe - Test train split data for supervised learning with Multiple Linear Regression
# Saving feature names for later use
feature_list = list(data_nut_gen.columns)
# X= multiple input variables, y = output variable 'LifeExpectancy'
X, y = data_nut_gen.iloc[:, 4:12], data_nut_gen.iloc[:, 3]
# Split training and test data sets
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.33, random_state=42) # set to 42 means the results will be the same each time the split is run
# Split val and test data
y_val = train_test_split(y, shuffle=True)
# Check for dimensionalilty of split data
print('Training Features Shape:', X_train.shape)
print('Training Labels Shape:', y_train.shape)
print('Testing Features Shape:', X_test.shape)
print('Testing Labels Shape:', y_test.shape)
# 2. Aufgabe
# Define the multiple Linear Regression model and train it
lr_model = LinearRegression().fit(X_train, y_train)
# Predict the target values for the given features
y_pred = lr_model.predict(X_test)
# Plot prediction Array
print ('Predicted Life Expectancy per Country in Years: \n', y_pred)
# Calculate the absolute errors
errors = abs(y_pred - y_test)
# Calculate mean absolute percentage error (MAPE)
mape = 100 * (errors / y_test)
# Calculate and display accuracy
accuracy = 100 - np.mean(mape)
print('Mean Absolute Percentage Error:', round(mape.mean(), 2), '%.')
print('Accuracy:', round(accuracy, 2), '%.')
# The mean squared error
print('Mean squared error: %.2f'
% mean_squared_error(y_test, y_pred))
# The mean absolute error
print('Mean absolute error: %.2f'
% mean_absolute_error(y_test, y_pred))
# The coefficient of determination: 1 is perfect prediction
print('Coefficient of determination: %.2f'
% r2_score(y_test, y_pred))
# 3. Aufgabe
# The coefficients
coeff_df = pd.DataFrame(lr_model.coef_, X.columns, columns=['Coefficient'])
print(coeff_df)
Die Merkmale, welche einen hohen Einfluss auf das Ergebnis y des Models haben können mittels deren Koeffizienten dargestellt und verglichen werden.
In der dargestellten Tabelle lässt sich ablesen, dass bei der Erhöhung eines Merkmals um eine Einheit sich der Output um den entsprechenden Wert erhöht, bzw. verringert. Bei der Erhöhung des Merkmals x8 KcalOilFat um eine Einheit würde sich daher die Lebenserwartung um 0.001017 verringern.
Insgesamt haben die Merkmale x5 AnnualVegetableConsumptionPerCapita,x7 KcalSugar, sowie x4 AnnualFruitConsumptionPerCapita die größte Auswirkung und damit die höchste Bedeutung für den Output y.
# Random Forest Regression model
from sklearn.ensemble import RandomForestRegressor
from sklearn.preprocessing import MinMaxScaler
from sklearn.model_selection import cross_val_score, GridSearchCV
# 4. Aufgabe
# Instantiate model with 1000 decision trees
rf_model = RandomForestRegressor(n_estimators=1000, max_depth=7, random_state=42)
# Train the model on training data
rf_model.fit(X_train, y_train)
# Use the forest's predict method on the test data
y_rf_pred = rf_model.predict(X_test)
# Calculate the absolute errors
errors = abs(y_rf_pred - y_test)
# Print out the mean absolute error (mae)
print('Mean Absolute Error:', round(np.mean(errors), 2), 'years.')
# The mean squared error
print('Mean squared error: %.2f'
% mean_squared_error(y_test, y_rf_pred))
# The mean absolute error
print('Mean absolute error: %.2f'
% mean_absolute_error(y_test, y_rf_pred))
# The coefficient of determination: 1 is perfect prediction
print('Coefficient of determination: %.2f'
% r2_score(y_test, y_rf_pred))
# Calculate mean absolute percentage error (MAPE)
mape = 100 * (errors / y_test)
# Calculate and display accuracy
accuracy = 100 - np.mean(mape)
print('Accuracy:', round(accuracy, 2), '%.')
feature_list
# 5. Aufgabe
# Check for relative importance of the feature variables
# Get numerical feature importances
importances = list(rf_model.feature_importances_)
# List of tuples with variable and importance
feature_importances = [(feature, round(importance, 2)) for feature, importance in zip(feature_list[3:], importances)]
# Sort the feature importances by most important first
feature_importances = sorted(feature_importances, key = lambda x: x[1], reverse = True)
# Print out the feature and importances
[print('Variable: {:20} Importance: {}'.format(*pair)) for pair in feature_importances]
# Set the style
plt.style.use('fivethirtyeight')
# list of x locations for plotting
x_values = list(range(len(importances)))
# Make a bar chart
plt.bar(x_values, importances, orientation = 'vertical')
# Tick labels for x axis
plt.xticks(x_values, feature_list, rotation='vertical')
# Axis labels and title
plt.ylabel('Importance')
plt.xlabel('Variable')
plt.title('Variable Importances')
Untersuchung der Bedeutung von Merkmalsvariablen für die Performance des Models. Die mittels Skicit-Learn zurückgegebenen Werte der Bedeutsamkeit je Merkmal stellen dar, wie sehr die Einbeziehung einer bestimmten Variable die Vorhersage des Random Forest Regression Algorithmus verbessert. Mittels den tabellarisch aufgelisteten Werten kann ein Vergleich zwischen den Merkmalsvariablen angestrebt werden.
Ganz oben steht dabei das Merkmal AnnualHealthcareExpPerCapita. Dies sagt aus, dass der beste Prädikator für die Lebenserwartung die jährlichen Ausgaben in das Gesundheitssystem eines Staates pro Kopf sind. Das zweit wichtigste Merkmal wird unter GDPperCapita gelistet. Je höher das Bruttoinlandsprodukt (BPI) eines Landes ist desto höher der Wohlstand der Einzelpersonen und desto höher die Lebenserwartungen innerhalb einer Industrienation. Einen ebenso hohen Einfluss hat der Konsum von Fleisch, Ölen und Fetten auf die Lebenserwartung.
Dagegen haben der jährliche pro Kopf Frucht und Gemüse Konsum die geringste Bedeutung für die korrekte Vorhersage der jährlichen Lebenserwartung der Bevölkerung eines Landes. Bei zukünftigen Implementierungen des Modells können diese Variablen daher auch entfernt werden, ohne die Leistung des Algorithmus einzuschränken.
Der R²-Score ist ein Bestimmungsmaß zu Festlegung der Güte eines trainierten Linear Regression Models. Eine abhängige Variable $y_{i}$ lässt sich als die Summe aus dem vorhergesagten Wert $^y_{i}$ der Abweichung ( Varianz ) zwischen einem beobachteten und dem vom Modell vorhergesagten Wert $e_{i}$ ( Residuum ) berechnen. Je nach verwendeter Anzahl unabhängiger Variablen $x_{i}$ handelt es sich dabei um eine einfache Regression ( nur eine unabhängige Variable ) oder um eine multiple Regression ( mehrere unabhängige Variablen ).
Wichtig ist dabei zu verstehen, wie gut die unabhängigen Variablen dafür geeignet sind, um die Varianz der abhängigen Variabel $y_{i}$ zu erklären, bzw. neue Werte hervorzusagen. Hierfür ist der R²-Score , welcher einen Wert zwischen 0 und 1 erreichen kann geeignet. Je näher der Wert des R²-Score am Wert 0 liegt, desto ungeeigneter sind die gewählten Variablen, um $y_{i}$ vorherzusagen. Dies kann auch als poor model fit bezeichnet werden. Auf der anderen Seite kann man sagen, je näher der Wert an 1 liegt, desto näher ist die Regressionsgerade an den Eingabewerten.
Das Linear Regression Model konnte eine Accuracy von 93.54%, mit einem Mean Absolute Error (MAE) von 4.46% und einem R²-Score von 45% erreichen, während das Random Forest Regression Modell unter Training mit den selben unabhängigen Variablen $x_{i}$ deutlich bessere Werte, mit einer Accuracy von 95.1%, mit einem Mean Absolute Error (MAE) von 3.23% und R²-Score von 65% erreichen konnte.
Auch die Wichtigkeit der einzelnen, verwendeten Merkmale unterscheidet sich zwischen dem Linear Regression Model (LRM) und dem Random Forest Regression Model (RFRM). So hat beim LRM das Merkmal AnnualVegetableConsumptionPerCapita den größten, positiven Regressions Koeffizienten, d.h. vergrößert sich der Wert von xi um eine Einheit, so würde sich auch der Mittelwert der abhängigen Variable yi vergrößern. Ein negativer Wert zeigt jedoch auf, dass hier das Gegenteil der Fall wäre. Dies würde suggerieren, dass es für das Training des Models Sinn macht, den statistisch nicht signifikaten Variable $x_{i}$ aus den verwendeten Merkmalsvariablen zu entfernen, da diese die Präzision des Ergebnisses sogar verringern kann. Auch der R² Wert fällt relativ gering aus, was darauf hinweisen kann, dass die verwendeten unabhängigen Variblen statistisch nicht signifikant sind für die Varianz der abhängigen Variablen $y_{i}$. Beim RFRM Model hat dagegen die Merkmalsvariable AnnualHealthcareExpPerCapita die stärkste Relevanz als Prädikator für die abhängige Variable $y_{i}$.
Bei einer visuellen Analyse auf Linearität der Trainingsdaten konnte bei den unabhängigen Variablen AnnualHealthcareExpPerCapita als auch GDPperCapita eine geringere lineare Abhängigkeit aufgezeigt werden. Das LRM ist jedoch ein lineares Modell, welches gut geeignet ist für Daten mit einer linearen Form. Dies könnte eine Erklärung sein für das schlechtere Abschneiden einzelner Datensätze beim R²-Score sein, im Verhältnis zum RFRM. Auf der anderen Seite hat ein RFRM Schwierigkeiten die Linearkombinationen einer großen Anzahl von Merkmalen zu modellieren.
DailyCaloriesPerCapita im Dataframe nutrition_information für alle Jahre von 1960 bis 2012 einen Wert hat. Konstruieren Sie dann ein numpy-Array $X$, in dem das Element $C_{i,j}$ in Zeile $i$, Spalte $j$, den DailyCaloriesPerCapita-Wert des Landes $i$ im $j.ten$ Jahr zwischen 1960 und 2012 enthält.DailyCaloriesPerCapita-Wertes - also die Zeilen des Arrays $X$. Verwenden Sie hierfür den K-Means Algorithmus von scikit-learn. Stellen Sie dabei die Clusteranzahl auf $k=5$ ein. DailyCaloriesPerCapita-Wertes aller zum jeweiligen Cluster gehörenden Länder als Line-Graph dargestellt ist.from sklearn.cluster import KMeans
# 1. Aufgabe
X = nutrition_information.copy()
X = X.reset_index()
X = X[["Entity", "Year", "DailyCaloriesPerCapita"]]
X = X.set_index("Entity")
X = X.loc[X["Year"].isin(range(1961,2013))] #ab 1961 da für das Jahr 1960 nur China einen Wert hat
#Tabelle in passende Form bringen
X = X.pivot(index=X.index, columns='Year')['DailyCaloriesPerCapita']
X = X.dropna()
Xnumpy = X.to_numpy()
Xnumpy
# 2. Aufgabe
# KMeans anwenden
kmeans = KMeans(n_clusters=5, random_state=0).fit(X)
# Cluster zuordnen
X["Cluster"] = kmeans.labels_
# 3. Aufgabe
# Cluster plotten
for i in range(5):
toPlot = X.loc[X['Cluster'] == i]
toPlot = toPlot.drop(['Cluster'], axis=1)
toPlot.T.plot(figsize=(20,10), title="Cluster " + str(i))
Der k-Means-Algorithmus ist ein Rechenverfahren, das sich für die Gruppierung von Objekten, die sogenannte Clusteranalyse, einsetzen lässt. Dank der effizienten Berechnung der Clusterzentren und dem geringen Speicherbedarf eignet sich der Algorithmus sehr gut für die Analyse großer Datenmengen, wie sie im Big-Data-Umfeld üblich sind. Der Algorithmus ist in der Lage, aus einer Menge ähnlicher Objekte mit einer vorher bekannten Anzahl von Gruppen die jeweiligen Zentren der Cluster zu ermitteln. Da es sich um ein sehr effizientes Verfahren handelt, das mit vielen verschiedenen Datentypen zurecht kommt, und der Speicherbedarf gering ist, eignet sich der k-Means-Algorithmus für die Datenanalyse im Big-Data-Umfeld.
Die Laufzeit des Algorithmus ist linear zur Anzahl der vorhandenen Datenpunkte und die Anzahl der Schleifendurchläufe zur Ermittlung der Clusterzentren ist klein.
Jede Gruppe wird durch einen ihr zugehörigen Mittelpunkt repräsentiert. Das Ziel des k-Means-Clustering-Algorithmus ist das automatische Berechnen dieser k-Mittelpunkte (sog. Cluster-Mittelpunkte), welche die bekannten Daten am besten repräsentieren. In unserem Fall ist k = 5
Zu Beginn der Berechnung werden fünf zufällig gewählte Mittelpunkte gewählt. Anschließend wird jeder Messwert dem am nächsten liegenden Cluster-Mittelpunkt zugeordnet. Für jeden Mittelpunkt wird nun das geometrische Zentrum seiner ihm zugeordneten Messwerte ermittelt. Dorthin wird der Mittelpunkt verschoben. Die letzten beiden Schritte werden nun theoretisch so lange wiederholt, bis sich die Positionen der Mittelpunkte nicht mehr wesentlich verändern. Durch die Verschiebung kann sich auch die Zuordnung der Daten zu den Mittelpunkten ändern. Da nicht genau an jedem Mittelpunkt ein Datensatz vorhanden ist, wird der geometrisch jeweils am nächsten liegende Datensatz verwendet. Ziel ist es, dass die verschiedenen Objekte innerhalb einer Gruppe sich nach der erfolgten Einteilung möglichst ähnlich sind.
Der k-Means-Algorithmus findet Anwendung auf Daten oder Objekte in einem n-dimensionalen Raum. Das Verfahren verläuft in folgenden Schritten:
1. Wahl von k-Punkten als Anfangszentren der Berechnung
2. Zuordnung der Datenpunkte zu den verschiedenen Clustern auf Basis des Abstands zu den Zentren
3. Neuberechnung der Clusterzentren
4. Wiederholung ab Schritt 2 – bis sich die Lage der Zentren nicht mehr ändert
In den ersten Durchläufen des Algorithmus treten noch große Änderungen der Zentren auf. Mit zunehmenden Schleifendurchläufen werden die Veränderungen immer kleiner. Wesentlich für einen effizienten Ablauf des Algorithmus ist die Wahl der Anfangszentren.
In echten Fällen benutzt man häufig wesentlich mehr als zwei Daten-Dimensionen. Es ist nicht ungewöhnlich, dass ein einzelner Datensatz aus zehn und mehr Komponenten besteht – was die Visualisierung und Anschaulichkeit an dieser Stelle allerdings erschwert hätte.In unserem Beispiel war die Anzahl k der Mittelpunkte vorgegeben. In der Praxis ist die Anzahl nicht immer klar spezifiziert. Dieser Herausforderung begegnen wir mit geschicktem Ausprobieren, d. h. es werden verschiedene Werte für k systematisch getestet und die jeweilige Güte der Lösung bewertet.
Ein Problem bei der Anwendung des k-Means-Algorithmus stellt die Vorgabe der Anzahl der Cluster und die Wahl der Anfangszentren dar. Die gefundene Lösung ist stark von den gewählten Startpunkten abhängig. Zu Wahl der Startpunkte ist die Kenntnis einer gewissen Clusterstruktur notwendig, die aus Voranalysen der Daten stammen könnte. In vielen Fällen erfolgt der Durchlauf des Algorithmus mit unterschiedlichen Startwerten. Die verschiedenen Ergebnisse liefern Hinweise auf eine möglichst plausible Struktur der Cluster. Verwendet man eine ungeeignete Anzahl von Clusterzentren als Startwerte, können sich unter Umständen komplett andere Lösungen oder ungeeignete Clustereinteilungen ergeben.
Auch problematisch für den k-Means-Algorithmus sind Datenmengen, die sich überlappen oder in Teilen nahtlos ineinander übergehen. In diesen Fällen ist das k-Means-Verfahren nicht in der Lage, die verschiedenen Gruppen zuverlässig voneinander zu trennen. Daten mit hierarchischen Clusterstrukturen werden ebenfalls nicht unterstützt. Sind Ausreißer in der Datenmenge vorhanden, können diese das Ergebnis stark verfälschen, da k-Means keine Ausreißer erkennt und jedes Objekt einem Cluster zuordnet. In diesen Fällen ist vor der Auswertung mit dem k-Means-Algorithmus eine Bereinigung der Daten (Noisereduktion) durchzuführen.
Der k-Means-Algorithmus findet in vielen Bereichen Anwendung. Aufgrund seiner Effizienz und des geringen Speicher- und Rechenbedarfs eignet er sich für die Datenanalyse großer Datenmengen im Big-Data-Umfeld. In der Bildverarbeitung wird k-Means häufig zur Segmentierung der Bilddaten eingesetzt. Mit den Ergebnissen des Algorithmus ist beispielsweise die Trennung von Vorder- und Hintergrund oder das Erkennen von einzelnen Objekten möglich. Ein weiterer wichtiger Anwendungsbereich sind das Marketing und die Analyse des Kundenverhaltens. k-Means findet in vorliegenden Kundendaten verschiedene Gruppen von Kunden mit jeweils ähnlichem Kundenverhalten. Die von k-Means ermittelten homogenen Gruppen lassen sich klar voneinander trennen. Mithilfe spezifischer Marketingmaßnahmen sind die Gruppen effizienter und mit größerer Erfolgsaussicht ansprechbar.os.system('jupyter nbconvert --to html yourNotebook.ipynb')
import os
os.system('jupyter nbconvert --to html Project1_Global_HealthData_App.ipynb')